/**
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.api.adwords.starterkit.Synchronizer;

import com.google.api.adwords.starterkit.db.keys.CampaignKey;
import com.google.api.adwords.starterkit.db.keys.NetworkTargetKey;
import com.google.api.adwords.starterkit.db.structures.CampaignStructure;
import com.google.api.adwords.starterkit.db.structures.NetworkTargetStructure;
import com.google.api.adwords.starterkit.db.tables.AdGroupTable;
import com.google.api.adwords.starterkit.db.tables.CampaignTable;
import com.google.api.adwords.starterkit.db.tables.NetworkTargetTable;
import com.google.api.adwords.starterkit.settings.StarterKitSettings;
import com.google.api.adwords.starterkit.utils.DumpExceptions;
import com.google.api.adwords.starterkit.utils.ServiceUtils;
import com.google.api.adwords.starterkit.utils.Utils;
import com.google.api.adwords.v201101.cm.ApiError;
import com.google.api.adwords.v201101.cm.ApiException;
import com.google.api.adwords.v201101.cm.Bid;
import com.google.api.adwords.v201101.cm.BiddingTransition;
import com.google.api.adwords.v201101.cm.Budget;
import com.google.api.adwords.v201101.cm.BudgetOptimizer;
import com.google.api.adwords.v201101.cm.Campaign;
import com.google.api.adwords.v201101.cm.CampaignOperation;
import com.google.api.adwords.v201101.cm.CampaignPage;
import com.google.api.adwords.v201101.cm.CampaignReturnValue;
import com.google.api.adwords.v201101.cm.CampaignServiceInterface;
import com.google.api.adwords.v201101.cm.ConversionOptimizer;
import com.google.api.adwords.v201101.cm.ManualCPC;
import com.google.api.adwords.v201101.cm.ManualCPCAdGroupBids;
import com.google.api.adwords.v201101.cm.ManualCPM;
import com.google.api.adwords.v201101.cm.ManualCPMAdGroupBids;
import com.google.api.adwords.v201101.cm.Money;
import com.google.api.adwords.v201101.cm.Operator;
import com.google.api.adwords.v201101.cm.Predicate;
import com.google.api.adwords.v201101.cm.PredicateOperator;
import com.google.api.adwords.v201101.cm.PricingModel;
import com.google.api.adwords.v201101.cm.Selector;

import org.apache.log4j.Logger;

import java.rmi.RemoteException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The Class CampaignSynchronizer is responsible to keep all Campaign 
 * information from AdWords system synchronized with local DB.
 * 
 * @author rodrigo.f.vale@gmail.com (Rodrigo Vale)
 */
public class CampaignSynchronizer {
  protected static final Logger log = Logger.getLogger("StarterKit");
  protected StarterKitSettings starterKitSetting;
  protected ServiceUtils serviceUtils;
  protected CampaignTable campaignTable;
  protected AdGroupTable adGroupTable;
  protected NetworkTargetTable networkTargetTable;

  public CampaignSynchronizer(StarterKitSettings starterKitSetting, 
      Connection conn) {
    setServiceUtils(new ServiceUtils(starterKitSetting));
    this.starterKitSetting = starterKitSetting;

    campaignTable = new CampaignTable(conn);
    adGroupTable = new AdGroupTable(conn);
    networkTargetTable = new NetworkTargetTable(conn);
  }

  /**
   * Save campaigns table to AdWords.
   */
  public void put() {
    log.debug("Preparing Campaigns to be sent to AdWords");
    
    if (starterKitSetting.getCampaignSettings().getAutoPause()) {
      campaignTable.autoPause();
    }
    campaignTable.grantBanned();
    
    int tries = 0;

    while (tries < starterKitSetting.getRetriesOnError()) {
      HashSet<CampaignOperation> operations = new HashSet<CampaignOperation>();
      try {
        log.info("Updating/Creating Campaigns - Try: " + tries);
        log.debug("Loading DB");
        ResultSet rs = campaignTable.getModified();
        log.debug("Processing Tuples");
        while (rs.next()) {
          CampaignStructure campStruct = campaignTable.getResultSet(rs);

          // Create campaign.
          Campaign campaign = new Campaign();
          campaign.setName(campStruct.getName());
          campaign.setEndDate(campStruct.getEndDate());
          campaign.setStatus(campStruct.getStatus());

          // Create budget.
          Budget budget = new Budget();
          budget.setPeriod(campStruct.getBudgetPeriod());
          budget.setAmount(new Money(null, campStruct.getBudgetAmount()));
          budget.setDeliveryMethod(campStruct.getDeliveryMethod());
          campaign.setBudget(budget);

          CampaignOperation operation = new CampaignOperation();
          if ((campStruct.getCampaignId() == 0)
              && (starterKitSetting.getCampaignSettings().getAutoCreation())) {
            if (campStruct.getBiddingStrategy().equals("MANUALCPC")) {
              campaign.setBiddingStrategy(new ManualCPC());

            } else if (campStruct.getBiddingStrategy().equals("MANUALCPM")) {
              campaign.setBiddingStrategy(new ManualCPM());
            } else if (campStruct.getBiddingStrategy().equals(
                "CONVERSIONOPTIMIZER")) {
              ConversionOptimizer conversionOptimizer 
                  = new ConversionOptimizer();
              if (campStruct.getPricingModel().equals("CLICKS")) {
                conversionOptimizer.setPricingModel(PricingModel.CLICKS);
              } else if (campStruct.getPricingModel().equals("IMPRESSIONS")) {
                conversionOptimizer.setPricingModel(PricingModel.IMPRESSIONS);
              } else if (campStruct.getPricingModel().equals("CONVERSIONS")) {
                conversionOptimizer.setPricingModel(PricingModel.CONVERSIONS);
              }
              campaign.setBiddingStrategy(conversionOptimizer);
            } else if (campStruct.getBiddingStrategy()
                .equals("BUDGETOPTIMIZER")) {
              BudgetOptimizer budgetOptimizer = new BudgetOptimizer();
              budgetOptimizer.setBidCeiling(new Money(null, campStruct
                  .getBidCeiling()));
              campaign.setBiddingStrategy(budgetOptimizer);
            }
            operation.setOperand(campaign);
            operation.setOperator(Operator.ADD);
          } else {
            if ((campStruct.isBiddingModified())
                && (!campStruct.getOldBiddingStrategy().equals(
                    campStruct.getBiddingStrategy()))) {
              BiddingTransition biddingTransition = new BiddingTransition();
              if (campStruct.getBiddingStrategy().equals("MANUALCPC")) {
                biddingTransition.setTargetBiddingStrategy(new ManualCPC());

                if (!campStruct.getOldBiddingStrategy().equals(
                    "BUDGETOPTIMIZER")) {
                  long defaultCPC = starterKitSetting.getAdGroupSettings()
                      .getCpc();
                  Money money = new Money(null, defaultCPC);
                  Bid bid = new Bid(money);
                  ManualCPCAdGroupBids adGroupBids = new ManualCPCAdGroupBids(
                      null, bid, bid, bid, true);
                  biddingTransition.setExplicitAdGroupBids(adGroupBids);
                }
              } else if (campStruct.getBiddingStrategy().equals("MANUALCPM")) {
                biddingTransition.setTargetBiddingStrategy(new ManualCPM());

                if (campStruct.getOldBiddingStrategy().equals("MANUALCPC")) {
                  long defaultCPM = starterKitSetting.getAdGroupSettings()
                      .getCpm();
                  Money money = new Money(null, defaultCPM);
                  Bid bid = new Bid(money);
                  ManualCPMAdGroupBids adGroupBids = new ManualCPMAdGroupBids(
                      null, bid);
                  biddingTransition.setExplicitAdGroupBids(adGroupBids);
                }
              } else if (campStruct.getBiddingStrategy().equals(
                  "CONVERSIONOPTIMIZER")) {
                ConversionOptimizer conversionOptimizer 
                    = new ConversionOptimizer();
                if (campStruct.getPricingModel().equals("CLICKS")) {
                  conversionOptimizer.setPricingModel(PricingModel.CLICKS);
                } else if (campStruct.getPricingModel().equals("IMPRESSIONS")) {
                  conversionOptimizer.setPricingModel(PricingModel.IMPRESSIONS);
                } else if (campStruct.getPricingModel().equals("CONVERSIONS")) {
                  conversionOptimizer.setPricingModel(PricingModel.CONVERSIONS);
                }
                biddingTransition.setTargetBiddingStrategy(conversionOptimizer);
              } else if (campStruct.getBiddingStrategy().equals(
                  "BUDGETOPTIMIZER")) {
                BudgetOptimizer budgetOptimizer = new BudgetOptimizer();
                budgetOptimizer.setBidCeiling(new Money(null, campStruct
                    .getBidCeiling()));
                biddingTransition.setTargetBiddingStrategy(budgetOptimizer);

                if (!campStruct.getOldBiddingStrategy().equals("MANUALCPC")) {
                }
              }
              operation.setBiddingTransition(biddingTransition);
            } else {
              if (campStruct.getBiddingStrategy().equals("MANUALCPC")) {
                campaign.setBiddingStrategy(new ManualCPC());
              } else if (campStruct.getBiddingStrategy().equals("manualcpm")) {
                campaign.setBiddingStrategy(new ManualCPM());
              } else if (campStruct.getBiddingStrategy().equals(
                  "CONVERSIONOPTIMIZER")) {
                ConversionOptimizer conversionOptimizer 
                    = new ConversionOptimizer();
                if (campStruct.getPricingModel().equals("CLICKS")) {
                  conversionOptimizer.setPricingModel(PricingModel.CLICKS);
                } else if (campStruct.getPricingModel().equals("IMPRESSIONS")) {
                  conversionOptimizer.setPricingModel(PricingModel.IMPRESSIONS);
                } else if (campStruct.getPricingModel().equals("CONVERSIONS")) {
                  conversionOptimizer.setPricingModel(PricingModel.CONVERSIONS);
                }
                campaign.setBiddingStrategy(conversionOptimizer);
              } else if (campStruct.getBiddingStrategy().equals(
                  "BUDGETOPTIMIZER")) {
                BudgetOptimizer budgetOptimizer = new BudgetOptimizer();
                budgetOptimizer.setBidCeiling(new Money(null, campStruct
                    .getBidCeiling()));
                campaign.setBiddingStrategy(budgetOptimizer);
              }
            }
            campaign.setId(campStruct.getCampaignId());
            operation.setOperand(campaign);
            operation.setOperator(Operator.SET);
          }
          operations.add(operation);
        }
        rs.close();
      } catch (SQLException e3) {
        log.debug("SQL Error on saveCampaigns. Error: " + e3.getMessage());
        log.debug("StarterKit will try to move on. Some updates may be lost.");
      }

      boolean error = processOperations(operations);

      // If no policy error we don't need to run the process again
      if (!error) {
        tries = starterKitSetting.getRetriesOnError();
      } else {
        tries++;
        Utils.sleep(starterKitSetting.getSleepOnPolicyError());
      }
    }
    campaignTable.resetStatus();
  }

  /**
   * Save processed updates from Campaigns to AdWords.
   * 
   * @param operations
   *          The updates to be sent to AdWords
   */
  protected boolean processOperations(HashSet<CampaignOperation> operations) {
    log.debug("To be updated: " + operations.size());
    CampaignServiceInterface campaignService;
    campaignService = getServiceUtils().getCampaignService();
    boolean error = false;
    if (operations.size() > 0) {
      List<HashSet<CampaignOperation>> blocks = Utils.split(operations, 1000);
      for (HashSet<CampaignOperation> opBlocks : blocks) {
        CampaignOperation[] op = opBlocks
            .toArray(new CampaignOperation[opBlocks.size()]);

        // Add ad group criteria.
        CampaignReturnValue result = null;
        try {
          result = campaignService.mutate(op);
          // Display ad group criteria.
          if (result != null && result.getValue() != null) {
            for (Campaign campaign : result.getValue()) {
              CampaignKey campaignKey = new CampaignKey(campaign.getName());
              campaignTable.setIsUpdated(campaignKey, false);
              campaignTable.setId(campaignKey, campaign.getId());
            }
          }
          log.debug("Updated " + op.length + " Campaings");
        } catch (ApiException e1) {
          DumpExceptions.dumpApiException(e1);
          error = true;
          Pattern pattern = Pattern.compile("operations\\[(\\d+)\\].*");
          for (ApiError apiError : e1.getErrors()) {
            Matcher matcher = pattern.matcher(apiError.getFieldPath());

            log.debug(apiError.getApiErrorType());
            if (matcher.matches()) {
              int i = Integer.parseInt(matcher.group(1));
              String campaignName = op[i].getOperand().getName();
              CampaignKey campaignKey = new CampaignKey(campaignName);
              campaignTable.setIsUpdated(campaignKey, false);
              log.debug("Canceling update for campaign: " + campaignName);
            }
          }
        } catch (RemoteException e) {
          error = true;
          DumpExceptions.dumpRemoteException(e);
        }
      }
    }
    return error;
  }

  /**
   * Load campaigns from adwords.
   */
  public void get() {
    log.debug("Processing Campaigns JSON Objects");
    if ((starterKitSetting.getDbSyncType() != SynchronizerType.NO)
        && (starterKitSetting.doesUseSandbox())) {
      log.fatal("StarterKit was set up to use AdWords API sandbox account. "
          + "That is fine! :). But the SyncType was define to a value "
          + "different of 'NO'. This is not fine. :(. At this moment, "
          + "StarterKit uses reports to syncronize local db with AdWords. "
          + "Sandbox do not support reports. In that case, to avoid many "
          + "synconization problems, is better to stop now. Modify the "
          + "settings.xml file to SyncType = 'NO' and keep in mind that while "
          + "using Sandbox, StarterKit will not syncronize content from local "
          + "db and Sandbox.");
      System.exit(1);
    }
    // Get the CampaignService.
    CampaignServiceInterface campaignService;
    campaignTable.resetIsBanned(true);
    try {
      campaignService = getServiceUtils().getCampaignService();

      // Create selector.
      Selector selector = new Selector();

      if (starterKitSetting.getDownloadDeletedCampaigns()) {
        Predicate predicate = new Predicate("Status", PredicateOperator.IN,
            new String[] { "ACTIVE", "PAUSED", "DELETED" });
        selector.setPredicates(new Predicate[] { predicate });
      } else {
        Predicate predicate = new Predicate("Status", PredicateOperator.IN,
            new String[] { "ACTIVE", "PAUSED" });
        selector.setPredicates(new Predicate[] { predicate });
      }

      selector.setFields(new String[] { "Id", "Name", "Status", "StartDate",
          "EndDate", "Period", "BiddingStrategy", "Settings",
          "TargetGoogleSearch", "TargetSearchNetwork", "TargetContentNetwork",
          "TargetContentContextual", "TargetPartnerSearchNetwork", "Amount" });

      // Get selected campaigns.
      CampaignPage page = campaignService.get(selector);
      // HashSet<Long> campagnIds = new HashSet<Long>();
      HashMap<Long, String> campaigns = new HashMap<Long, String>();

      // Display campaigns.
      if (page.getEntries() != null) {
        for (Campaign campaign : page.getEntries()) {
          campaigns.put(campaign.getId(), campaign.getName());
          CampaignStructure campaignStructure = new CampaignStructure();

          campaignStructure.setCampaignId(campaign.getId());
          campaignStructure.setName(campaign.getName());
          campaignStructure.setBudgetAmount(campaign.getBudget().getAmount()
              .getMicroAmount());
          campaignStructure.setBudgetPeriod(campaign.getBudget().getPeriod());
          campaignStructure
              .setBudgetPeriod(campaignStructure.getBudgetPeriod());
          campaignStructure.setStartDate(campaign.getStartDate());
          campaignStructure.setEndDate(campaign.getEndDate());
          campaignStructure.setStatus(campaign.getStatus());

          campaignStructure.setOldBiddingStrategy("");
          campaignStructure.setPricingModel("");
          campaignStructure.setBidCeiling(0);
          campaignStructure
              .setDeliveryMethod(campaign.getBudget().getDeliveryMethod());

          campaignStructure.setBiddingStrategy(campaign.getBiddingStrategy()
              .getBiddingStrategyType());
          campaignStructure.setBiddingStrategy(campaignStructure
              .getBiddingStrategy().toLowerCase());

          if (campaignStructure.getBiddingStrategy()
              .equals("conversionoptimizer")) {
            ConversionOptimizer conversionOptimizer = 
                (ConversionOptimizer) campaign.getBiddingStrategy();
            
            PricingModel pricingModel = conversionOptimizer.getPricingModel();
            if (pricingModel != null) {
              campaignStructure
                  .setPricingModel(pricingModel.getValue().toLowerCase());
            }
          }

          if (campaignStructure.getBiddingStrategy()
              .equals("budgetoptimizer")) {
            BudgetOptimizer budgetOptimizer = (BudgetOptimizer) campaign
                .getBiddingStrategy();
            if (budgetOptimizer.getBidCeiling() != null) {
              campaignStructure.setBidCeiling(budgetOptimizer.getBidCeiling()
                  .getMicroAmount());
            }
          }

          CampaignKey campaignKey = new CampaignKey(campaign.getName());
          CampaignStructure campStructDB = campaignTable.get(campaignKey);
          campaignStructure.setOldBiddingStrategy("");

          // New campaigns from AdWords weren't created by StarterKit.
          // Lets keep them untouched!
          if (campStructDB == null) {
            campaignStructure.setBudgetModifiable(false);
            campaignStructure.setEndDateModifiable(false);
            campaignStructure.setStatusModifiable(false);

            campaignStructure.setBanned(false);

            campaignTable.save(campaignStructure, false, false);
          } else {
            campaignStructure.setLocalId(campStructDB.getLocalId());
            campaignStructure.setBudgetModifiable(campStructDB
                .isBudgetModifiable());
            campaignStructure.setEndDateModifiable(campStructDB
                .isEndDateModifiable());
            campaignStructure.setStatusModifiable(campStructDB
                .isStatusModifiable());

            if (!campaignStructure.getBiddingStrategy().equals(
                campStructDB.getBiddingStrategy())) {
              campaignStructure.setBudgetModifiable(false);
            }

            if (campaignStructure.getBudgetAmount() != campStructDB
                .getBudgetAmount()) {
              campaignStructure.setBudgetModifiable(false);
            }

            if (!campaignStructure.getEndDate().equals(
                campStructDB.getEndDate())) {
              campaignStructure.setEndDateModifiable(false);
            }

            if (!campaignStructure.getStatus()
                .equals(campStructDB.getStatus())) {
              campaignStructure.setStatusModifiable(false);
            }

            campaignStructure.setBanned(false);
            campaignTable.save(campaignStructure, false, false);
          }

          NetworkTargetKey networkTargetKey 
              = new NetworkTargetKey(campaignStructure.getLocalId());
          HashSet<NetworkTargetStructure> networkTargetsFromDB = 
              networkTargetTable.get(networkTargetKey);

          HashSet<NetworkTargetStructure> networkTargetsFromAdWords 
              = new HashSet<NetworkTargetStructure>();

          if (campaign.getNetworkSetting().getTargetContentContextual()) {
            NetworkTargetStructure networkTarget = new NetworkTargetStructure();
            networkTarget.setLocalCampaignId(campaignStructure.getLocalId());
            networkTarget.setCampaignId(campaignStructure.getCampaignId());
            networkTarget.setNetwork("CONTENT_CONTEXTUAL");
            networkTargetsFromAdWords.add(networkTarget);
          }

          if (campaign.getNetworkSetting().getTargetContentNetwork()) {
            NetworkTargetStructure networkTarget = new NetworkTargetStructure();
            networkTarget.setLocalCampaignId(campaignStructure.getLocalId());
            networkTarget.setCampaignId(campaignStructure.getCampaignId());
            networkTarget.setNetwork("CONTENT_NETWORK");
            networkTargetsFromAdWords.add(networkTarget);
          }

          if (campaign.getNetworkSetting().getTargetGoogleSearch()) {
            NetworkTargetStructure networkTarget = new NetworkTargetStructure();
            networkTarget.setLocalCampaignId(campaignStructure.getLocalId());
            networkTarget.setCampaignId(campaignStructure.getCampaignId());
            networkTarget.setNetwork("GOOGLE_SEARCH");
            networkTargetsFromAdWords.add(networkTarget);
          }

          if (campaign.getNetworkSetting().getTargetPartnerSearchNetwork()) {
            NetworkTargetStructure networkTarget = new NetworkTargetStructure();
            networkTarget.setLocalCampaignId(campaignStructure.getLocalId());
            networkTarget.setCampaignId(campaignStructure.getCampaignId());
            networkTarget.setNetwork("CPA_REFERRAL");
            networkTargetsFromAdWords.add(networkTarget);
          }

          if (campaign.getNetworkSetting().getTargetSearchNetwork()) {
            NetworkTargetStructure networkTarget = new NetworkTargetStructure();
            networkTarget.setLocalCampaignId(campaignStructure.getLocalId());
            networkTarget.setCampaignId(campaignStructure.getCampaignId());
            networkTarget.setNetwork("SEARCH_NETWORK");
            networkTargetsFromAdWords.add(networkTarget);
          }

          if (!NetworkTargetTable.equals(networkTargetsFromAdWords,
              networkTargetsFromDB)) {
            networkTargetTable.del(networkTargetKey);
            networkTargetTable.save(networkTargetsFromAdWords);
            campaignTable.setNetFlag(campaignKey, true);
          }
        }
      }
    } catch (ApiException e) {
      DumpExceptions.dumpApiException(e);
    } catch (RemoteException e) {
      DumpExceptions.dumpRemoteException(e);
    }
    campaignTable.resetStatus();
  }

  /**
   * Load campaigns performance from adwords.
   */
  public void getStats() {
    log.debug("Processing Campaigns Performance JSON Objects");
    campaignTable.resetStats();
    try {
      ResultSet rs = adGroupTable.getStats();

      while (rs.next()) {
        campaignTable.saveStatsBatch(
            rs.getFloat("Ctr"),
            rs.getLong("Clicks"),
            rs.getFloat("Cost"),
            rs.getLong("Impressions"),
            rs.getLong("Conversions"),
            rs.getLong("Transactions"),
            (float) rs.getDouble("averagePosition"),
            (float) rs.getDouble("adGroupMaxCpa"),
            (float) rs.getDouble("firstPageCpc"),
            rs.getLong("campaignId"));
      }
      rs.close();
      campaignTable.executeStatsBatch();     
    } catch (SQLException e) {
      DumpExceptions.dumpSQLException(e);
    }
  }

  public void setServiceUtils(ServiceUtils serviceUtils) {
    this.serviceUtils = serviceUtils;
  }

  public ServiceUtils getServiceUtils() {
    return serviceUtils;
  }
}
